#!/usr/bin/env python
# -*- coding: utf-8 -*-


"""
    Part of Seagull - The Pyhon Pages engine
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    page_providers:
      Abstraction for a the page storage which can be a filesystem,
      database, or network storage. Each provider is an abstraction for
      one type of storage, and such a provider should also handle
      reading/writing of metadata and reading/writing the compiled
      version of the pages.
"""
__author__ = 'Jonathan Slenders <jonathan at slenders dot be>'
__license__ = 'GNU LGPL'

__all__ = ('PythonPagesDirectoryProvider', 'BinaryFilesDirectoryProvider')

import os
import posixpath
import codecs
import exceptions

# TODO: User readable name doesn't have to be here, it's sufficent to be in the config file

#===========[ Abstract classes ]===========

class PageProvider(object):
	""" Base class for any page provider """
	def __init__(self, name):
		self.name = name # User readable name

	def walk(self):
		""" Yields for every page his path """
		raise NotImplementedError()

	def lookup_page(self, page_path):
		""" Should return either a PythonPage or BinaryPage object """
		raise NotImplementedError()

	def create_directory(self, path):
		""" Creates a new directory when this one does not already exists """
		raise NotImplementedError()

class PythonPageProvider(PageProvider):
	"""
	Base class for any python page provider
	"""
	def __init__(self, name):
		PageProvider.__init__(self, name)

	def create_page(self, page_path, content):
		""" Creates a new python page at this path """
		raise NotImplementedError()


class BinaryPageProvider(PageProvider):
	"""
	Base class for any binary page provider
	"""
	def __init__(self, name):
		PageProvider.__init__(self, name)

	def stream_data_content(self, page_path, output):
		raise NotImplementedError()

class Directory(object):
	"""
	Directory helper class
	"""
	def __init__(self, provider, parent_directory, name):
		self._provider = provider
		self.parent = parent_directory
		self.name = name
		self.path = posixpath.join(self.parent, self.name)

	def walk(self):
		for p in self._provider.walk(self.path):
			yield p

class PythonPage(object):
	"""
	PythonPage helper class
	"""
	def __init__(self):
		self.directory = None
		self.name = None
		self.url_suffix = ''
		self.path = None
	def delete_previous_compile_result(self): pass
	def get_source(self): pass
	def save_source(self, content): pass
	def save_source(self): pass
	def get_compiled_page(self): pass
	def save_compiled_page(self, content): pass
	def get_meta_data(self): pass
	def save_meta_data(self, content): pass

class BinaryPage(object):
	"""
	BinaryPage helper class
	"""
	def __init__(self):
		self.url_suffix = ''

	def get_file_extension(self): pass
	def stream(self, output): pass



#===========[ Private helper methods ]===========

def is_valid_path(path):
	return \
		isinstance(path, unicode) and \
		path[0] == '/' and \
		path.find('..') == -1

#===========[ Implementations ]===========

class PythonPagesDirectoryProvider(PythonPageProvider):
	class _PythonPage(PythonPage):
		"""
		Page object generated by a PythonPagesDirectoryProvider
		"""
		def __init__(self, provider, directory, name, url_suffix):
			self._provider = provider
			self.directory = directory # posix url directory, starting with '/'
			self.name = name # filename without '.page' extension
			self.url_suffix = url_suffix
			self.path = posixpath.join(self.directory, self.name)

			self._compiled_page_directory = os.path.join(
				provider.compiled_pages_directory,
				self.directory.lstrip('/').replace('/', os.sep))

		def _get_source_path(self):
			return os.path.join(
				self._provider.pages_directory,
				self.directory.lstrip('/').replace('/', os.sep),
				'%s.page' % self.name)

		def _get_compiled_path(self):
			return os.path.join(self._compiled_page_directory, '%s.py' % self.name)

		def _get_metadata_path(self):
			return os.path.join(self._compiled_page_directory, '%s.xml' % self.name)

		def get_source(self):
			page_path = self._get_source_path()

			if os.path.isfile(page_path):
				return codecs.open(page_path, 'r', 'utf-8').read()
			else:
				raise PageNotFoundException(page_path)

		def save_source(self, content):
			page_path = self._get_source_path()

			if os.path.isfile(page_path):
				codecs.open(page_path, 'w', 'utf-8').write(content),
			else:
				raise PageNotFoundException(page_path)

		def has_been_changed(self):
			try:
				compiled_path = self._get_compiled_path()
				return not os.path.isfile(compiled_path) or (os.stat(self._get_source_path())[8] > os.stat(compiled_path)[8])

			except exceptions.OSError:
				return False

		def delete_previous_compile_result(self):
			compiled_page_path = self._get_compiled_path()
			metadata_path = self._get_compiled_path()

			if os.path.isfile(compiled_page_path):
				os.remove(compiled_page_path)

			if os.path.isfile(metadata_path):
				os.remove(metadata_path)

		def get_compiled_page(self):
			compiled_page_path = self._get_compiled_path()

			if os.path.isfile(compiled_page_path):
				return open(compiled_page_path, 'rb').read()
			else:
				raise PageNotFoundException(self.path)

		def save_compiled_page(self, content):
			if not os.path.isdir(self._compiled_page_directory):
				os.makedirs(self._compiled_page_directory)

			codecs.open(os.path.join(self._compiled_page_directory, '%s.py' % self.name), 'w', 'utf-8').write(content),

		def get_meta_data(self):
			metafile = self._get_metadata_path()

			if os.path.isfile(metafile):
				return open(metafile, 'r').read();
			else:
				raise NotCompiledException(self.path)

		def save_meta_data(self, content):
			assert(isinstance(content, str))

			if not os.path.isdir(self._compiled_page_directory):
				os.makedirs(self._compiled_page_directory)

			open(self._get_metadata_path(), 'w').write(content),

	def __init__(self, name, pages_directory, compiled_pages_directory):
		""" PythonPagesDirectoryProvider constructor """
		PythonPageProvider.__init__(self, name)

		self.pages_directory = pages_directory
		self.compiled_pages_directory = compiled_pages_directory

	def walk(self, path=u'/'):
		assert(path[0] == '/')

		def _walk(path, directory):
			for f in os.listdir(path):
				# Skip hidden files
				if f.startswith('.'):
					pass

				elif os.path.isfile(os.path.join(path, f)):
					(base, ext) = os.path.splitext (f)
					if ext == '.page':
						yield self._PythonPage(self, directory, base, '/')

				elif os.path.isdir(os.path.join(path, f)):
					yield Directory(self, directory, f)

		full_path = self.pages_directory + path.replace('/', os.sep)

		if os.path.isdir(full_path):
			for p in _walk(full_path, path):
				yield p

	def lookup_page(self, page_path):
		assert(isinstance(page_path, unicode))

		(directory, name, url_suffix) = self.__find_path(page_path)
		return self._PythonPage(self, posixpath.join('/', '/'.join(directory)), name, url_suffix)

	def create_directory(self, path):
		""" Creates a new directory when this one does not already exists """
		assert is_valid_path(path)
		
		parts = filter(lambda x: len(x), path.split('/'))

		# The parent should be a directory
		if os.path.isdir(os.path.join(self.pages_directory, os.sep.join(parts[:-1]))):
			directory = os.path.join(self.pages_directory, os.sep.join(parts))

			# Create directory
			if not os.path.isdir(directory):
				os.mkdir(directory)
		else:
			raise DirectoryNotFoundException('/'.join(parts[:-1]))

	def create_page(self, path, content):
		""" Create new python page and fill with this source """
		assert is_valid_path(path)
		
		parts = filter(lambda x: len(x), path.split('/'))

		# The parent should be a directory
		if os.path.isdir(os.path.join(self.pages_directory, os.sep.join(parts[:-1]))):
			filename = os.path.join(self.pages_directory, os.sep.join(parts) + '.page')

			# Write to file (if this page doesn't exists already
			if os.path.exists(filename):
				raise PageExistsException(path)
			else:
				codecs.open(filename, 'w', 'utf-8').write(content),
		else:
			raise DirectoryNotFoundException('/'.join(parts[:-1]))


	def __find_path(self, path):
		""" Returns (directory, name, suffix) """
		assert is_valid_path(path)

		parts = filter(lambda x: len(x), path.split('/'))

		name_index = 0

		# Look for the directory part
		for i in range(len(parts)):
			if os.path.isdir(os.path.join(
					self.pages_directory,
					os.sep.join(parts[0:i+1])
						).encode('utf-8')):
				name_index = i+1
			else:
				break

		# Check whether this file exists
		if name_index < len(parts) and os.path.isfile(os.path.join(
					self.pages_directory,
					os.sep.join(parts[0:name_index]),
					u'%s.page' % parts[name_index]
						).encode('utf-8')):
			return (
				parts[0:name_index],
				parts[name_index],
				u'/%s' % '/'.join(parts[name_index+1:]))

		# Check whether an 'index' file exists in this directory
		# (a suffix isn't allowed in this case)
		elif name_index == len(parts) and os.path.isfile(os.path.join(
					self.pages_directory,
					os.sep.join(parts[0:name_index]),
					u'index.page'
						).encode('utf-8')):
			return (
				parts[0:name_index+1],
				u'index',
				u'/')
		else:
			raise PageNotFoundException(path)

class BinaryFilesDirectoryProvider(BinaryPageProvider):
	class _BinaryPage(BinaryPage):
		def __init__(self, provider, directory, name):
			BinaryPage.__init__(self)
			self._provider = provider
			self.directory = directory
			self.name = name
			self.path = posixpath.join(self.directory, self.name)

		def get_file_extension(self):
			(_, ext) = os.path.splitext(self.path)
			return ext
			
		def stream_data_content(self, output):
			full_path = os.path.join(
					self._provider.pages_directory,
					self.directory.lstrip('/').replace('/', os.sep),
					self.name)

			output.write(open(full_path, 'rb').read())

	def __init__(self, name, pages_directory):
		BinaryPageProvider.__init__(self, name)
		self.pages_directory = pages_directory

	def walk(self, path=u'/'):
		assert path[0] == '/'

		def _walk(path, directory):
			for f in os.listdir(path):
				# Skip hidden files
				if f.startswith('.'):
					pass

				elif os.path.isfile(os.path.join(path, f)):
					(base, ext) = os.path.splitext (f)
					if ext != '.page':
						yield self._BinaryPage(self, directory, base)

				elif os.path.isdir(os.path.join(path, f)):
					yield Directory(self, directory, f)


		full_path = self.pages_directory + path.replace('/', os.sep)

		if os.path.isdir(full_path):
			for p in _walk(full_path, path):
				yield p

	def lookup_page(self, page_path):
		path_parts = self.__find_path(page_path)
		return self._BinaryPage(self, '/'.join(path_parts[0:-1]), path_parts[-1])

	def __find_path(self, path):
		parts = filter(lambda x: len(x), path.split('/'))

		filename = os.path.join(self.pages_directory, os.sep.join(parts))
		if os.path.isfile(filename):
			if os.path.splitext(path)[1] == '.page':
				raise PageNotFoundException(path)
			else:
				return parts
		else:
			raise PageNotFoundException(path)

class MySqlProvider(PageProvider):
	# TODO
	pass

class PageNotFoundException(Exception):
	def __init__(self, page):
		Exception.__init__(self, 'Page "%s" not found' % page)

class DirectoryNotFoundException(Exception):
	def __init__(self, directory):
		Exception.__init__(self, 'Directory "%s" not found' % directory)


class PageExistsException(Exception):
	def __init__(self, path):
		Exception.__init__(self, 'Cannot create page "%s", file does already exist' % path)


class NotCompiledException(Exception):
	def __init__(self, page):
		Exception.__init__(self, 'Page "%s" isn\'t yet compiled' % page)
